/** @flow */

import * as React from 'react';
import createDetectElementResize from './detect-element-resize';

type Size = {
    height: number;
    width: number;
};

interface Props {
    /** Function responsible for rendering children.*/
    children: (arg: Size) => React.ReactNode;
    // children: (arg: Size) => React.Element<*>;

    /** Optional custom CSS class name to attach to root AutoSizer element.  */
    className?: string;

    /** Default height to use for initial render; useful for SSR */
    defaultHeight?: number;

    /** Default width to use for initial render; useful for SSR */
    defaultWidth?: number;

    /** Disable dynamic :height property */
    disableHeight: boolean;

    /** Disable dynamic :width property */
    disableWidth: boolean;

    /** Nonce of the inlined stylesheet for Content Security Policy */
    nonce?: string;

    /** Callback to be invoked on-resize */
    onResize: (arg: Size) => void;

    /** Optional inline style */
    style: object | null | undefined;
}

type State = {
    height: number;
    width: number;
};

type ResizeHandler = (element: HTMLElement, onResize: () => void) => void;

type DetectElementResize = {
    addResizeListener: ResizeHandler;
    removeResizeListener: ResizeHandler;
};

export default class AutoSizer extends React.Component<Props, State> {
    static defaultProps = {
        // eslint-disable-next-line @typescript-eslint/no-empty-function
        onResize: () => {},
        disableHeight: false,
        disableWidth: false,
        style: {}
    };

    state = {
        height: this.props.defaultHeight || 0,
        width: this.props.defaultWidth || 0
    };

    _parentNode: HTMLElement | null | undefined;
    _autoSizer: HTMLElement | null | undefined;
    _window: any; // uses any instead of Window because Flow doesn't have window type
    _detectElementResize: DetectElementResize | undefined;

    componentDidMount() {
        const { nonce } = this.props;
        if (
            this._autoSizer &&
            this._autoSizer.parentNode &&
            this._autoSizer.parentNode.ownerDocument &&
            this._autoSizer.parentNode.ownerDocument.defaultView &&
            this._autoSizer.parentNode instanceof
                this._autoSizer.parentNode.ownerDocument.defaultView.HTMLElement
        ) {
            // Delay access of parentNode until mount.
            // This handles edge-cases where the component has already been unmounted before its ref has been set,
            // As well as libraries like react-lite which have a slightly different lifecycle.
            this._parentNode = this._autoSizer.parentNode;
            this._window = this._autoSizer.parentNode.ownerDocument.defaultView;

            // Defer requiring resize handler in order to support server-side rendering.
            // See issue #41
            this._detectElementResize = createDetectElementResize(nonce, this._window);
            this._detectElementResize.addResizeListener(this._parentNode, this._onResize);

            this._onResize();
        }
    }

    componentWillUnmount() {
        if (this._detectElementResize && this._parentNode) {
            this._detectElementResize.removeResizeListener(this._parentNode, this._onResize);
        }
    }

    render() {
        const { children, className, disableHeight, disableWidth, style } = this.props;
        const { height, width } = this.state;

        // Outer div should not force width/height since that may prevent containers from shrinking.
        // Inner component should overflow and use calculated width/height.
        // See issue #68 for more information.
        const outerStyle: any = { overflow: 'visible' };
        const childParams: any = {};

        if (!disableHeight) {
            outerStyle.height = 0;
            childParams.height = height;
        }

        if (!disableWidth) {
            outerStyle.width = 0;
            childParams.width = width;
        }

        /**
     * TODO: Avoid rendering children before the initial measurements have been collected.
     * At best this would just be wasting cycles.
     * Add this check into version 10 though as it could break too many ref callbacks in version 9.
     * Note that if default width/height props were provided this would still work with SSR.
    if (
      height !== 0 &&
      width !== 0
    ) {
      child = children({ height, width })
    }
    */

        return (
            <div
                className={className}
                ref={this._setRef}
                style={{
                    ...outerStyle,
                    ...style
                }}
            >
                {children(childParams)}
            </div>
        );
    }

    _onResize = () => {
        const { disableHeight, disableWidth, onResize } = this.props;

        if (this._parentNode) {
            // Guard against AutoSizer component being removed from the DOM immediately after being added.
            // This can result in invalid style values which can result in NaN values if we don't handle them.
            // See issue #150 for more context.

            const height = this._parentNode.offsetHeight || 0;
            const width = this._parentNode.offsetWidth || 0;

            const win = this._window || window;
            const style = win.getComputedStyle(this._parentNode) || {};
            const paddingLeft = parseInt(style.paddingLeft, 10) || 0;
            const paddingRight = parseInt(style.paddingRight, 10) || 0;
            const paddingTop = parseInt(style.paddingTop, 10) || 0;
            const paddingBottom = parseInt(style.paddingBottom, 10) || 0;

            const newHeight = height - paddingTop - paddingBottom;
            const newWidth = width - paddingLeft - paddingRight;

            if (
                (!disableHeight && this.state.height !== newHeight) ||
                (!disableWidth && this.state.width !== newWidth)
            ) {
                this.setState({
                    height: height - paddingTop - paddingBottom,
                    width: width - paddingLeft - paddingRight
                });

                onResize({ height, width });
            }
        }
    };

    _setRef = (autoSizer: HTMLElement | null | undefined) => {
        this._autoSizer = autoSizer;
    };
}
